/**
 * Tests the propagation of Operation FCV in the oplog's `versionContext` field for:
 * - Non-applyOps entries from operations outside transactions.
 * - applyOps entries for batched writes.
 * - applyOps entries for unprepared transactions.
 * - applyOps entries for prepared transactions.
 *
 * @tags: [
 *   required_fcv_83,
 *   requires_replication,
 *   uses_transactions,
 *   uses_prepare_transaction
 * ]
 */
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {PrepareHelpers} from "jstests/core/txns/libs/prepare_helpers.js";

let rst = new ReplSetTest({nodes: 2});
rst.startSet();
rst.initiate();
const conn = rst.getPrimary();
const db = conn.getDB("test");

const currentFCV = assert.commandWorked(db.adminCommand({getParameter: 1, featureCompatibilityVersion: 1}))
    .featureCompatibilityVersion.version;

// Force operations to use an Operation FCV, so they emit the `versionContext` field in their oplog
// entries. This is required since not all operations use an OFCV yet.
const originalMongoRunCommand = Mongo.prototype.runCommand;
Mongo.prototype.runCommand = function runCommandSpy(dbName, cmdObj, options) {
    cmdObj.versionContext = {OFCV: currentFCV};
    return originalMongoRunCommand.apply(this, arguments);
};

function findOneInOplog(filter) {
    const oplogEntry = db.getSiblingDB("local").oplog.rs.findOne(filter);
    assert(oplogEntry, "Could not find an oplog entry for filter: " + tojson(filter));
    return oplogEntry;
}

// Verifies that oplog entry format for a standalone entry (not a transaction nor a batched write).
// We expect those entries to have the versionContext field, except for no-op entries.
function verifyStandaloneOplogEntry(oplogEntry) {
    assert(
        !(oplogEntry.op === "c" && oplogEntry.o.applyOps),
        "Unexpected format for standalone oplog entry: " + tojson(oplogEntry),
    );

    if (oplogEntry.op === "n") {
        assert(!oplogEntry.versionContext, "Unexpected versionContext in no-op entry: " + tojson(oplogEntry));
    } else {
        assert.eq({OFCV: currentFCV}, oplogEntry.versionContext, tojson(oplogEntry));
    }
}

(function testStandaloneOperations() {
    const coll = db.no_txn;

    const insertResult = assert.commandWorked(db.runCommand({insert: coll.getName(), documents: [{x: 1}]}));
    verifyStandaloneOplogEntry(findOneInOplog({ts: insertResult.operationTime}));

    const updateResult = assert.commandWorked(
        db.runCommand({update: coll.getName(), updates: [{q: {x: 1}, u: {$set: {x: 2}}}]}),
    );
    verifyStandaloneOplogEntry(findOneInOplog({ts: updateResult.operationTime}));

    const dropResult = assert.commandWorked(db.runCommand({drop: coll.getName()}));
    verifyStandaloneOplogEntry(findOneInOplog({ts: dropResult.operationTime}));

    const noopResult = assert.commandWorked(
        db.adminCommand({
            appendOplogNote: 1,
            data: {msg: "test oplog note"},
        }),
    );
    verifyStandaloneOplogEntry(findOneInOplog({ts: noopResult.operationTime}));
})();

// Verifies that oplog entry format for a batched write.
// We expect it to specify versionContext on the oplog entry, but *not* for each sub-operation,
// since batched writes are generated by a single operation (with its single OFCV).
function verifyBatchedWritesEntry(oplogEntry) {
    assert(
        oplogEntry.op === "c" && oplogEntry.o.applyOps && !oplogEntry.txnNumber,
        "Unexpected format for batched writes oplog entry:" + tojson(oplogEntry),
    );

    assert.eq({OFCV: currentFCV}, oplogEntry.versionContext, tojson(oplogEntry));
    for (const nestedOp of oplogEntry.o.applyOps) {
        assert(!nestedOp.versionContext, tojson(oplogEntry));
    }
}

(function testBatchedWrites() {
    const coll = db.getCollection("batched_writes");

    const insertResult = assert.commandWorked(db.runCommand({insert: coll.getName(), documents: [{x: 1}, {x: 2}]}));
    verifyBatchedWritesEntry(findOneInOplog({ts: insertResult.operationTime}));

    const deleteResult = assert.commandWorked(
        db.runCommand({
            delete: coll.getName(),
            deletes: [{q: {x: {$gt: 0}}, limit: 0}],
        }),
    );
    verifyBatchedWritesEntry(findOneInOplog({ts: deleteResult.operationTime}));
})();

// Verifies that oplog entry format for a (prepared or unprepared) transaction.
// We expect it to not specify versionContext for each sub-operation, but not on the oplog entry,
// since transactions can be generated by multiple operations (each with its OFCV).
function verifyTransactionOplogEntry(oplogEntry) {
    assert(
        oplogEntry.op === "c" && oplogEntry.o.applyOps && oplogEntry.txnNumber,
        "Unexpected format for transaction oplog entry:" + tojson(oplogEntry),
    );

    assert(!oplogEntry.versionContext, tojson(oplogEntry));
    for (const nestedOp of oplogEntry.o.applyOps) {
        assert.eq({OFCV: currentFCV}, nestedOp.versionContext, tojson(oplogEntry));
    }
}

(function testUnpreparedTransaction() {
    const session = conn.startSession();
    session.startTransaction();
    const sessionDB = session.getDatabase(db.getName());
    const coll = sessionDB.getCollection("unprepared_txn");

    assert.commandWorked(coll.createIndex({x: 1}));
    assert.commandWorked(coll.insertOne({x: 123}));
    assert.commandWorked(coll.updateOne({x: 123}, {$set: {x: 321}}));
    const commitResult = assert.commandWorked(session.commitTransaction_forTesting());
    verifyTransactionOplogEntry(findOneInOplog({ts: commitResult.operationTime}));
})();

(function testPreparedTransaction() {
    const session = conn.startSession();
    session.startTransaction();
    const sessionDB = session.getDatabase(db.getName());
    const coll = sessionDB.getCollection("prepared_txn");

    // Collections can't be created in a prepared transaction, so do it outside
    assert.commandWorked(db.createCollection(coll.getName()));

    assert.commandWorked(coll.insertOne({x: 1}));
    assert.commandWorked(coll.updateOne({x: 1}, {$set: {x: 2}}));
    const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
    const commitResult = assert.commandWorked(PrepareHelpers.commitTransaction(session, prepareTimestamp));

    verifyTransactionOplogEntry(findOneInOplog({ts: prepareTimestamp}));
    verifyStandaloneOplogEntry(findOneInOplog({ts: commitResult.operationTime}));
})();

Mongo.prototype.runCommand = originalMongoRunCommand;

rst.stopSet();
